From: Matteo Collina Date: Wed, 1 Jul 2026 07:43:01 +0000 (+0200) Subject: http2: defer rst stream while in scope X-Git-Tag: archive/raspbian/18.20.4+dfsg-1_deb12u3+rpi1^2~5 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=4fcec73db4a0d753f435fe9fdacc8372b1be54a5;p=nodejs.git http2: defer rst stream while in scope Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs-private/node-private/pull/921 Refs: https://hackerone.com/reports/3833629 Reviewed-By: Rafael Gonzaga CVE-ID: CVE-2026-56848 origin: https://github.com/nodejs/node/commit/daa6d25e3dceb30edb832a778ec0610c8bc2dd12 bug: https://nodejs.org/en/blog/vulnerability/july-2026-security-releases#http2-re-entrant-send-can-cause-heap-use-after-free-cve-2026-56848---high Gbp-Pq: Topic sec Gbp-Pq: Name CVE-2026-56848.patch --- diff --git a/src/node_http2.cc b/src/node_http2.cc index 2e7745f82..f2f856b44 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -2334,10 +2334,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) { // if RST_STREAM received is not in scope and added to the list // causing endpoint to hang. if (session_->is_in_scope() && is_stream_cancel(code)) { - session_->AddPendingRstStream(id_); - return; + session_->AddPendingRstStream(id_); + return; } + // If RST_STREAM is submitted while nghttp2 is processing callbacks for + // a refused stream, don't force purge pending data. Sending pending data + // here can re-enter nghttp2 and close streams that are still being used + // by the active receive operation. + if (session_->is_in_scope() && code == NGHTTP2_REFUSED_STREAM) { + FlushRstStream(); + return; + } // If possible, force a purge of any currently pending data here to make sure // it is sent before closing the stream. If it returns non-zero then we need diff --git a/test/parallel/test-http2-rst-stream-reentrancy.js b/test/parallel/test-http2-rst-stream-reentrancy.js new file mode 100644 index 000000000..8added7f9 --- /dev/null +++ b/test/parallel/test-http2-rst-stream-reentrancy.js @@ -0,0 +1,68 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const net = require('net'); + +const PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'); + +function frame(type, flags, sid, payload = Buffer.alloc(0)) { + const header = Buffer.alloc(9); + header.writeUIntBE(payload.length, 0, 3); + header[3] = type; + header[4] = flags; + header.writeUInt32BE(sid & 0x7fffffff, 5); + return Buffer.concat([header, payload]); +} + +function goaway(lastStreamID, code) { + const payload = Buffer.alloc(8); + payload.writeUInt32BE(lastStreamID, 0); + payload.writeUInt32BE(code, 4); + return frame(7, 0, 0, payload); +} + +function headers(sid) { + return frame(1, 0x05, sid, Buffer.from([ + 0x82, // :method: GET + 0x86, // :scheme: http + 0x84, // :path: / + 0x41, 0x01, 0x78, // :authority: x + ])); +} + +const server = http2.createServer(); + +server.listen(0, common.mustCall(() => { + const socket = net.connect(server.address().port); + let sent = false; + + socket.on('connect', common.mustCall(() => { + socket.write(Buffer.concat([PREFACE, frame(4, 0, 0)])); + })); + + socket.on('error', () => {}); + + socket.on('data', common.mustCallAtLeast(() => { + if (sent) + return; + sent = true; + + socket.write(frame(4, 1, 0)); + socket.write(headers(1)); + + setImmediate(() => { + socket.write(Buffer.concat([ + goaway(0, 0), + headers(3), + headers(5), + headers(7), + ])); + setTimeout(() => socket.destroy(), common.platformTimeout(50)); + }); + })); + + socket.on('close', common.mustCall(() => server.close())); +}));